#pragma once
#include "CScripts.h"
#include "CScrLanguage.h"

class CDecompiler
{
	static FILE		*					s_hInFile,
					*					s_hOutFile;
	static string						s_sBuffer;
	static BYTE		*					s_pInBuffer,
					*					s_pInCurrent;
	static tOpcode						s_sLastOpcode;
	static CLabel	*					s_pCurrentLabel,
					*					s_pLastPrintedLabel;
	static CSubScript	*				s_pSubScript;
	static DWORD						s_dwScriptBeginOffset,
										s_dwOpcodeBeginOffset,
										s_dwCurrentOffset;
	static char							s_szScriptName[MAX_SCRIPT_NAME],
										s_szStringParam[MAX_STRING_LENGTH];
	static bool							s_bNot,
										s_bOr,
										s_bDoNewLine,
										s_bSecondPass;
	static int							s_nTotalNumConditions,
										s_nNumConditions,
										s_nNumLinesWritten,
										s_nIndentLevels,
										s_nNumBytesRead,
										s_nNumParams,
										s_nNumReturns,
										s_nInputSize;
	static tVarIndex					s_Var;
	static ArrayVar						s_ArrayVar;
	static uParam						s_Params[MAX_PARAMS],
										s_Returns[MAX_PARAMS];

public:
	static int Decompile(FILE *hInFile, FILE *hOutFile);
	static void DecompileLine();

	static void Init(FILE *hInFile, FILE *hOutFile)
	{
		s_nNumBytesRead = 0;
		s_nNumLinesWritten = 0;
		s_dwCurrentOffset = 0;
		s_hInFile = hInFile;
		s_hOutFile = hOutFile;
		s_sBuffer.clear();
		
		s_nInputSize = FileGetSize(hInFile);
		s_pInCurrent = s_pInBuffer = new BYTE[s_nInputSize];
		fread(s_pInBuffer, sizeof(char), s_nInputSize, hInFile);
	}

	static void Finalize()
	{
		delete[] s_pInBuffer;
		fwrite(s_sBuffer.c_str(), sizeof(char), s_sBuffer.size(), s_hOutFile);
	}

	static void Preline()
	{
		/*CLabel *s_pCurrentLabel = nullptr;

		if(!s_bSecondPass)
		{
			if(s_pSubScript)
			{
				s_pCurrentLabel = s_pSubScript->GetLabelByOffset(s_dwCurrentOffset);
			}
			else
			{
				// floppy
				//if(!(s_pCurrentLabel = g_pScript->FindLabel(s_dwCurrentOffset)))
				//{
				//	//s_pCurrentLabel = AddLabel(s_dwCurrentOffset);
				//}

				//if(!(s_pSubScript = g_pScript->GetSubScript(s_dwCurrentOffset)))
				//{
				//	s_pSubScript = s_pCurrentLabel ? g_pScript->AddSubScript(s_pCurrentLabel) : g_pScript->AddSubScript(s_dwCurrentOffset);
				//}
			}

			if(s_pCurrentLabel)
			{
				if(!s_pSubScript && s_pCurrentLabel->IsScript())
					s_pSubScript = g_pScript->AddSubScript(s_pCurrentLabel);

				s_dwScriptBeginOffset = s_pCurrentLabel->GetStart();

				if(s_pCurrentLabel->IsScript() || s_pCurrentLabel->IsMission())
				{
					s_dwScriptBeginOffset = s_dwCurrentOffset;
				}
			}
		}

		if(s_pCurrentLabel && s_pCurrentLabel != s_pLastPrintedLabel)
		{
			strcpy(g_szTempString, "\n");
			strcat(g_szTempString, s_pCurrentLabel->GetName());
			strcat(g_szTempString, ":\n\n");
			s_nNumLinesWritten += 4;
			Output(g_szTempString);
			s_pLastPrintedLabel = s_pCurrentLabel;
		}*/
	}

	static void Output(const char *szStr)
	{
		if(!s_sBuffer.empty() && !isspace(s_sBuffer[s_sBuffer.size()-1]))
		{
			Trace(" ");
			s_sBuffer.push_back(' ');
		}
		Trace(szStr);
		s_sBuffer += szStr;
	}

	static void DecompileError(const char *szStr)
	{
		Printf("ERROR: (%d) %s", s_nNumLinesWritten, szStr);
	}

	static void DecompileWarning(const char *szStr)
	{
		Printf("WARNING: (%d) %s", s_nNumLinesWritten, szStr);
	}

	inline static COpcode * ReadOpcode()
	{
		s_bDoNewLine = 1;
		s_nNumParams = 0;
		s_nNumReturns = 0;
		s_dwOpcodeBeginOffset = s_dwCurrentOffset;
		s_sLastOpcode = Read<tOpcode>();
		if(s_sLastOpcode & LOGICAL_NOT)
		{
			s_sLastOpcode &= ~LOGICAL_NOT;
			s_bNot = 1;
		}
		else s_bNot = 0;
		return COpcodes::Get(s_sLastOpcode);
	}

	inline static void ParseJump(int offset)
	{
		int cur = s_dwCurrentOffset;
		
		/*
			We're following GOTO's to ensure we decompile only the data the script may read at first. Then we store
			the skipped offset and parse it later, making the decompilation more accurate when raw hex is involved.
			May also help with high-construction.
		*/
		
		/*if(offset < 0)
		{
		}
		else
		{
			// Only follow this goto if it's going forward, so we're not stuck in a loop.
			if(cur < offset)
			{
				// Add the code after this goto to the list of segments to compile later.
				//{...}
				if(s_pCurrentLabel) s_pCurrentLabel->SetEnd(cur);
				
				WriteLabelName(g_szTempString, cur);
				
				//CLabel * pLabel = CLabels::Add(g_szTempString, cur);
				//CLabels::IndexOffset(pLabel);
				
				//pLabel->SetEnd(offset);
				//pLabel->WasSkippedOnFirstPass(true);

				SetPC(offset);
			}
		}*/
	}

	inline static void ParseOpcode(COpcode *pOpcode)
	{
		CCommand *pCommand;
		int idx;

		pCommand = pOpcode->Get<CCommand>();

		if(!pCommand) return;

		int func = CSyntax::GetOpcodeParseFunction(pOpcode);

		switch(func)
		{
		case SCRIPT_GOTO:
			idx = pCommand->GetFirstParamIndexOfExtendedType(ET_PARSABLE);
			ParseJump(s_Params[idx].i);
			break;

		default:;
		}
	}

	inline static void SetPC(size_t n)
	{
		s_pInCurrent = s_pInBuffer + n;
		s_dwCurrentOffset = n;
	}

	inline static void IncreasePC(size_t n)
	{
		s_pInCurrent += n;
		s_nNumBytesRead += n;
		s_dwCurrentOffset += n;
	}

	inline static bool IsEnd()
	{
		/*if(s_pCurrentLabel && s_pCurrentLabel->GetEnd())
			return s_pInCurrent >= (s_pInBuffer + s_pCurrentLabel->GetSize());
		else
			return s_pInCurrent >= (s_pInBuffer + s_nInputSize);*/
		return true;
	}

	template<typename T> inline static T Read()
	{
		T r = *(T*)s_pInCurrent;
		IncreasePC(sizeof(T));
		return r;
	}
	template<> inline static char Read<char>()
	{
		char r = *(char*)s_pInCurrent;
		IncreasePC(sizeof(char));
		return r;
	};
	template<> inline static short Read<short>()
	{
		short r = *(short*)s_pInCurrent;
		IncreasePC(sizeof(short));
		return r;
	};
	template<> inline static int Read<int>()
	{
		int r = *(int*)s_pInCurrent;
		IncreasePC(sizeof(int));
		return r;
	};
	template<> inline static float Read<float>()
	{
		float r = *(float*)s_pInCurrent;
		IncreasePC(sizeof(float));
		return r;
	};

	inline static uParam ReadParameter(tDataType type)
	{
		uParam p;
		switch(type)
		{
		case DT_FLOAT:
			p.f = Read<float>();
			break;
		case DT_DWORD:
			p.i = Read<int>();
			break;
		case DT_WORD:
			p.i = Read<short>();
			break;
		case DT_BYTE:
			p.i = Read<char>();
			break;
		default:
			p.i = 0;
			break;
		}
		s_Params[s_nNumParams] = p;
		return p;
	}

	inline static void ReadString(tDataType type)
	{
		BYTE size;
		switch(type)
		{
		case DT_VARLEN_STRING:
			size = Read<BYTE>();
			szncpy(s_szStringParam, (char*)s_pInCurrent, size+1);
			IncreasePC(size);
			break;
		case DT_STRING:
			szncpy(s_szStringParam, (char*)s_pInCurrent, 16);
			IncreasePC(16);
			break;
		case DT_TEXTLABEL:
		default:
			szncpy(s_szStringParam, (char*)s_pInCurrent, 8);
			IncreasePC(8);
			break;
		}
	}

	inline static void ReadArray(tDataType type)
	{
		s_ArrayVar.wOffsetVar = ReadVarIndex();
		s_ArrayVar.wIndexVar = ReadVarIndex();
		s_ArrayVar.bSize = Read<char>();
		s_ArrayVar.bType = Read<char>();
	}

	inline static tDataType ReadDataType()
	{
		return Read<tDataType>();
	}

	inline static tVarIndex ReadVarIndex()
	{
		return Read<tVarIndex>();
	}

	inline static void OutputNewline()
	{
		if(s_bDoNewLine)
		{
			Output("\n");
			s_nNumLinesWritten++;
		}
	}

	inline static void OutputParam(tDataType type, uParam *p)
	{
		switch(type)
		{
		case DT_TEXTLABEL:
		case DT_STRING:
			ReadString(type);
			strcpy(g_szTempString, s_szStringParam);
			break;
		case DT_VARLEN_STRING:
			ReadString(type);
			strcpy(g_szTempString, "\'");
			strcat(g_szTempString, s_szStringParam);
			strcat(g_szTempString, "\'");
			break;
		case DT_FLOAT:
			p->f = Read<float>();
			strcpy(g_szTempString, tostr(p->f));
			break;
		case DT_DWORD:
			p->i = Read<int>();
			strcpy(g_szTempString, tostr(p->i));
			break;
		case DT_WORD:
			p->i = Read<short>();
			strcpy(g_szTempString, tostr(p->i));
			break;
		case DT_BYTE:
			p->i = Read<char>();
			strcpy(g_szTempString, tostr(p->i));
			break;
		case DT_VAR_ARRAY:
		case DT_VAR_TEXTLABEL_ARRAY:
		case DT_VAR_STRING_ARRAY:
			ReadArray(type);
			sprintf(g_szTempString, "%dv[%d%c]", s_ArrayVar.wOffsetVar, s_ArrayVar.wIndexVar, s_ArrayVar.bGlobalIndex ? 'v' : 'l');
			break;
		case DT_LVAR_ARRAY:
		case DT_LVAR_TEXTLABEL_ARRAY:
		case DT_LVAR_STRING_ARRAY:
			ReadArray(type);
			sprintf(g_szTempString, "%dl[%d%c]", s_ArrayVar.wOffsetVar, s_ArrayVar.wIndexVar, s_ArrayVar.bGlobalIndex ? 'v' : 'l');
			break;
		case DT_VAR:
		case DT_VAR_STRING:
		case DT_VAR_TEXTLABEL:
			p->i = ReadVarIndex();
			
			if(CVariable *pVariable = CVariables::Get(p->i))
			{
				strcpy(g_szTempString, pVariable->GetName());
			}
			else
			{
				strcpy(g_szTempString, tostr(p->i));
				strcat(g_szTempString, "v");
			}
			break;
		case DT_LVAR:
		case DT_LVAR_STRING:
		case DT_LVAR_TEXTLABEL:
			p->i = ReadVarIndex();
			strcpy(g_szTempString, tostr(p->i));
			strcat(g_szTempString, "l");
			break;
		default:;
		}
		Output(g_szTempString);
	}

	inline static bool OutputParsedParam(COpcode *pOpcode, tDataType type, uParam *p)
	{
		// return true to prevent parameter by being outputted again
		/*int func = CSyntax::GetOpcodeParseFunction(pOpcode);
		switch(func)
		{
		case SCRIPT_IF:
			if(type & DT_ANYVALUE)
			{
				p->i = ReadParameter(type).i;
				if(p->i)
				{
					if(s_bOr = p->i >= LOGICAL_OR)
						p->i -= LOGICAL_OR;
					else if(p->i >= LOGICAL_AND)
						p->i -= LOGICAL_AND;
					s_nNumConditions = p->i + 2;
				}
				else
				{
					s_bOr = 0;
					s_nNumConditions = 0;
				}
				s_bDoNewLine = 0;
				s_nTotalNumConditions = s_nNumConditions;
				break;
			}
			return false;

		case SCRIPT_NAME:
			if(type & DT_ANYSTRING)
			{
				ReadString(type);
				szncpy(s_szScriptName, StringToLower(s_szStringParam, 8), 8);
				Output(s_szStringParam);
				
				if(!s_pSubScript)
				{
					// floppy
					if(!(s_pCurrentLabel = CLabels::Get(s_dwOpcodeBeginOffset)))
						s_pCurrentLabel = CLabels::Add(s_szScriptName, s_dwOpcodeBeginOffset);

					s_dwScriptBeginOffset = s_dwOpcodeBeginOffset;
					
					s_pSubScript = g_pScript->AddSubScript(s_pCurrentLabel);
					s_pSubScript->SetName(s_szScriptName);
				}
				else
				{
					s_pSubScript->SetName(s_szScriptName);
				}
				break;
			}
			return false;

		//case SCRIPT_GOTO:
		//	if(OutputLabel(type, p)) break;
		//	return false;

		case SCRIPT_START:
			if(type & DT_ANYVALUE)
			{
				p->i = ReadParameter(type).i;
				
				CLabel * pLabel;
				CSubScript * pSubScript;

				//if(!(pLabel = ->GetLabelByOffset(p->i))
				//{
				//	pLabel = AddLabel(p->i);
				//}

				if(!(pSubScript = g_pScript->GetSubScript(p->i)))
				{
					if(!(pLabel = CLabels::Get(p->i))) pLabel = AddLabelNoIndex(p->i);
					g_pScript->AddSubScript(pLabel);
				}
				else
				{
					if(!(pLabel = pSubScript->GetLabelByOffset(p->i))) pLabel = AddLabelNoIndex(p->i);
				}

				if(pLabel)
				{
					pLabel->IsScript(true);
					pLabel->IsReferenced(true);
				}

				//OutputLabel(type, p);
				Output(pLabel->GetName());
			}
			break;

		default:
 			if(CCommand *pCommand = pOpcode->Get<CCommand>())
			{
 				CParamType pt = pCommand->GetParam(s_nNumParams-1);
				if(pt.m_ExtendedType & ET_LABEL && type & DT_ANYVALUE)
				{
					int i = p->i = ReadParameter(type).i;

					CLabel * pLabel = nullptr;

					if(!(pLabel = s_pSubScript->GetLabelByOffset(p->i)))
					{
						if(!(pLabel = CLabels::Get(p->i)))
						{
							if(func == SCRIPT_START) pLabel = AddLabelNoIndex(p->i);
							else pLabel = AddLabel(p->i);
						}

						s_pSubScript->AddLabel(pLabel);
					}

					if(pLabel) Output(pLabel->GetName());
					else OutputLabel(type, p);

					pLabel->IsReferenced(true);

					if(func == SCRIPT_START) pLabel->IsScript(true);
					break;
				}
			}
			return false;
		}*/

		return true;
	}

	inline static CLabel * AddLabel(int offset)
	{
		WriteLabelName(g_szTempString, offset);
		
		// add the label and an index of it's offset for easy access
		CLabel *pLabel = CLabels::Add(g_szTempString, offset);
		CLabels::IndexOffset(pLabel);
		return pLabel;
	}

	inline static CLabel * AddLabelNoIndex(int offset)
	{
		WriteLabelName(g_szTempString, offset);
		return CLabels::Add(g_szTempString, offset);
	}

	inline static char * WriteLabelName(char *dst, int label_offset)
	{
		// TODO: Use label_offset to determine pre-defined label names
		if(label_offset < 0)
		{
			label_offset = -label_offset;
		}
		
		label_offset -= s_dwScriptBeginOffset;

		strcpy(dst, s_szScriptName);                                                                     
		strcat(dst, "_");
		strcat(dst, tohexstr(label_offset, nullptr, 0));
		return dst;
	}

	inline static void OutputLabel(tDataType type, uParam *p)
	{
		WriteLabelName(g_szTempString, p->i);
		Output(g_szTempString);
	}

	inline static void OutputLogicalOperator(COpcode *pCommand)
	{
		if(pCommand->IsConditional())
		{
			if(s_bNot) Output("NOT");
			if(s_nNumConditions)
			{
				if(s_nTotalNumConditions > s_nNumConditions)
				{
					//printf();
					if(s_bOr) Output("OR");
					else Output("AND");
				}
 				if(!--s_nNumConditions) s_nIndentLevels++;
			}
		}
	}

	inline static uParam * StoreReturn(tDataType type)
	{
		return &s_Returns[s_nNumReturns++];
	}

	inline static uParam * StoreParam(tDataType type)
	{
		return &s_Params[s_nNumParams++];
	}

	inline static void OutputHex(int num_bytes)
	{
		OutputNewline();
		Output("HEX");
		for(int n=0; n<num_bytes; n++)
		{
			if(n % MAX_HEX_BYTES_PER_LINE == 0) OutputNewline();
			BYTE b = Read<BYTE>();
			Output(tohexstr(b));
		}
		OutputNewline();
		Output("ENDHEX");
	}

	inline static void ProcessCommand(COpcode *pOpcode, int numParams, bool bReturn)
	{
		tDataType type;
		CCommand *pCommand = pOpcode->Get<CCommand>();
		for(int i=0; i<numParams; i++)
		{
			CParamType param = bReturn ? pCommand->GetReturn(i) : pCommand->GetParam(i);

			type = 0;

			if(param.GetExtendedType() & ET_IMMEDIATE_DATA)
			{
				switch(param.GetBasicType() & PT_ANYBASIC)
				{
				case PT_STRING:
				case PT_TEXTLABEL:
					IncreasePC(sizeof(char) * param.GetSize());
					ReadBytesFromFile(s_hInFile, s_szStringParam, param.GetSize());

					sprintf(g_szTempString, "'%s'", s_szStringParam);
					Output(g_szTempString);
					break;

				default:
					OutputNewline();
					Output("HEX");
					for(int n=0; n<param.GetSize(); n++)
					{
						if(n % MAX_HEX_BYTES_PER_LINE == 0) OutputNewline();

						BYTE b = s_szStringParam[n];
						strcat(g_szTempString, tohexstr(b));
						Output(g_szTempString);
					}
					OutputNewline();
					Output("ENDHEX");
					break;
				}
			}
			else
			{
				if(type = ReadDataType())
				{
					uParam *p;

					if(bReturn) p = StoreReturn(type);
					else p = StoreParam(type);

					if(!OutputParsedParam(pOpcode, type, p))
						OutputParam(type, p);

					if(param.GetExtendedType() & ET_VAR_EXTRA_PARAMS ||
						param.GetExtendedType() & ET_NUM_EXTRA_PARAMS)
						i--;

					ParseOpcode(pOpcode);
				}
			}
		}
	}
};